#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@NAME          : ppttest.py
@TIME          : 2024/12/04 17:41:57
@AUTHOR        : chenlip
@VERSION       : 0.0.1
@DESCRIPTION   : 测试PPTX文件操作


约束或依赖：
1- python-pptx 库目前不支持直接在母版版式（SlideLayout）或幻灯片母版（SlideMaster）中添加形状或文本框。
2- python-pptx 目前不支持获取母版页中的占位符的字体信息，只能获取幻灯片中的文本框的字体信息。
3- python-pptx 目前不支持在slide页面插入占位符。
4- 系统中需要预先安装 Font Awesome 6 Free Regular 字体，用于在母版页中添加图标。
    # 免费版
    `/otfs/Font Awesome 5 Free-Solid-900.otf`   # 实体图标
    `/otfs/Font Awesome 5 Free-Regular-400.otf` # 常规图标
    # 图标查找：https://fontawesome.com.cn/v5
5- 系统中需要预先安装微软雅黑字体，用于在母版页中添加中文文本。
6- 系统中需要预先安装Segoe UI Emoji字体，用于在母版页中添加Emoji图标。输入方法：Win+;，然后输入Emoji字符。

'''
# --------------------------引用--------------------------
import os, sys, json

from app.common.utils import Utils as ut
from pptx import Presentation, presentation, slide, shapes
from pptx.shapes import picture, placeholder
from pptx.util import Inches, Pt, Cm, Emu, Length
from pptx.enum.shapes import MSO_SHAPE, PP_PLACEHOLDER_TYPE
from pptx.enum.dml import MSO_COLOR_TYPE
from pptx.dml.color import RGBColor

from lxml import etree

from app.common.mdact import MdAct
# --------------------------引用--------------------------

_pptxtemppath = r'E:\python\wemedia\static\temps\pptx' # 模板文件路径
_pptoutpath = r'E:\python\wemedia\static\outs' # 输出文件路径
_pptstaticpath = r'E:\python\wemedia\static\images\pptx' # 静态文件路径, 用于存放一些静态插画，图片等

menu = ["创建PPTX文件", "添加幻灯片", "添加封面", "添加图片"]

def read_md(webpath:str=None) -> list:
    '''
        读取md文件
        :Args:
            - webpath: 网络路径
        :Return:
            - md文件内容
    '''
    # 读取
    if webpath is None:
        md_url = r'https://minio.51epub.com/static/md/2024/src/孩子厌学怎么办？科学诊断与有效治疗指南.md'
    else:
        md_url = webpath

    md_act = MdAct(md_url)
    menu = md_act.get_h2_title_lst()
    print(md_act.md_toc)

    # 分页数据
    _slp_page = md_act.md_slip_page()
    print("\n 分页情况：", _slp_page)

    return _slp_page

# func

def emu_to_s(emu, unit='px'):
    """
    将 EMU 转换为像素, 1 像素 = 9525 EMU, 1 厘米 = 360000 EMU, 1 英寸 = 914400 EMU, 1 磅 = 12700 EMU, 保留两位小数
    """
    _lenght = Length(emu)
    if unit == 'px':
        _px = emu / 9525
    elif unit == 'cm':
        _px = _lenght.cm
    elif unit == 'inch':
        _px = _lenght.inches
    elif unit == 'pt':
        _px = _lenght.pt
    elif unit == 'mm':
        _px = _lenght.mm
    elif unit == 'centipoints': # 整数长度，以百分之一点（1/7200 英寸）为单位。使用 内部，因为 PowerPoint 以厘米为单位存储字体大小
        _px = _lenght.centipoints # 1/100 pt

    return round(_px, 2)

def createpptx(templatefname:str="tp1.pptx", targetfname:str=None) -> str:
    '''
        创建一个pptx文件,
        :Args:
            - targetfname: 目标文件名，例如：'test.pptx'
            - templatefname: 模板文件名. 如果没有模板文件,则创建一个空白的pptx文件
        :Return:
            - pptx文件的路径
    '''
    if targetfname is None: targetfname = ut.CURDAY+"_test.pptx"

    pptxfile = os.path.join(_pptoutpath, targetfname)
    
    if os.path.exists(pptxfile): os.remove(pptxfile)
    if templatefname is None:
        prs = Presentation()
        prs.save(pptxfile)
    else:
        tempfn = os.path.join(_pptxtemppath, templatefname)
        prs = Presentation(tempfn)
        prs.save(pptxfile)
    
    return pptxfile

def copy_font_style(source_font, target_font):
    """
    复制字体样式
    """
    target_font.name = source_font.name
    # print(f"copy_font_style- 字体名称：{source_font.name}")
    target_font.size = source_font.size
    # print(f"copy_font_style- 字体大小：{source_font.size}")
    target_font.bold = source_font.bold
    target_font.italic = source_font.italic
    # 检查颜色类型是否为 RGB
    if source_font.color.type == MSO_COLOR_TYPE.RGB and source_font.color.rgb:
        target_font.color.rgb = source_font.color.rgb
    else:
        # 设置为默认颜色（黑色）
        target_font.color.rgb = RGBColor(0, 0, 0)

def add_textbox_with_style(slide, reference_ph, left, top, width, height, text):
    """
    在幻灯片上添加一个新的文本框，并复制参考占位符的样式
    """
    # 添加新的文本框
    new_shape = slide.shapes.add_textbox(left, top, width, height)
    text_frame = new_shape.text_frame
    #text_frame.clear()  # 清除默认段落

    # 添加新段落并设置文本
    new_paragraph = text_frame.add_paragraph()
    new_paragraph.text = text

    # 复制字体样式，reference_ph是定位符：10---BODY (2)
    reference_paragraph = reference_ph.text_frame.paragraphs[0]
    copy_font_style(reference_paragraph.font, new_paragraph.font)

    return new_shape

def set_run_font_properties(shape):
    """
    为文本框中的所有运行设置字体属性，确保属性被明确设置
    """
    for paragraph in shape.text_frame.paragraphs:
        for run in paragraph.runs:
            run.font.name = '小熊软糖微微甜'
            run.font.size = Pt(24)
            run.font.bold = True
            run.font.italic = False
            run.font.color.rgb = RGBColor(0, 0, 0)

def get_textbox_font_info(slide):
    """
    获取幻灯片中所有文本框的字体信息
    """
    for shape in slide.shapes:
        if not shape.has_text_frame:
            continue
        # set_run_font_properties(shape)  # 手动设置字体属性
        for paragraph in shape.text_frame.paragraphs:
            if paragraph.runs:
                for run in paragraph.runs:
                    font = run.font
                    font_name = font.name
                    font_size = font.size
                    if font_size:
                        font_size_pt = font_size.pt  # 转换为 Pt 单位
                    else:
                        font_size_pt = None
                    print(f"get_textbox_font_info run 文本内容：{shape.text};  文本字体名称：{font_name}, 字体大小：{font_size_pt}")
            else:
                font = paragraph.font
                font_name = font.name
                font_size = font.size
                if font_size:
                    font_size_pt = font_size.pt  # 转换为 Pt 单位
                else:
                    font_size_pt = None
                print(f"get_textbox_font_info 文本内容：{shape.text}; 文本字体名称：{font_name}, 字体大小：{font_size_pt}")

# 删除ppt中的某一页slide
def delete_slide(prs, slide_id:int):
    '''
        删除ppt中的某一页slide
        :Args:
            - prs: Presentation对象
            - slide_id: slide的id
        :Return:
    '''
    slide = prs.slides[slide_id]
    rID = prs.slides._sldIdLst[slide_id].rId
    prs.part.drop_rel(rID)
    del prs.slides._sldIdLst[slide_id]

    return prs

# 获取母版中某个版式的某个占位符的位置和长宽信息
def get_master_placehold_box(slide_layout, placeholder_idx:int=0) -> list:
    '''
        获取母版中某个版式的某个占位符的位置和长宽信息, [left, top, width, height]
        :Args:
            - slide_layout: 幻灯片母版中的版式
            - placeholder_idx: 定位符的索引
        :Return:
            - 返回占位符的位置和长宽信息, [left, top, width, height]
    '''
    _ph_box = []
    _unit = 'cm'

    # 获取文本占位符
    placeholder = None
    for shape in slide_layout.shapes:
        if shape.is_placeholder and shape.placeholder_format.idx == placeholder_idx:
            # 获取文本
            print(f"get_master_placehold_box -》 占位符 text: {shape.text}")
            placeholder = shape
            break
    
    if placeholder is not None:
        _l = emu_to_s(placeholder.left, _unit)
        _t = emu_to_s(placeholder.top, _unit)
        _w = emu_to_s(placeholder.width, _unit)
        _h = emu_to_s(placeholder.height, _unit)
        _ph_box = [_l, _t, _w, _h]
        print(f"get_master_placehold_box -> 占位符位置和长宽: {_ph_box}")

    else:
        print("未找到指定的文本占位符")
    
    return _ph_box

# get_master_placehold_box

def get_placeholder_xml(slide_layout, placeholder_idx):
    """
    获取指定版式页中某个定位符的内部 XML
    """
    for shape in slide_layout.placeholders:
        if shape.placeholder_format.idx == placeholder_idx:
            ph_element = shape.element
            ph_xml = etree.tostring(ph_element, pretty_print=True, encoding='unicode')
            return ph_xml
    return None

# 获取母版页中的定位符的xml中获取的字体信息(字体名称，字体大小，字体颜色)
def get_master_placehold_font_info_by_xml(ph_xml:str) -> list:
    '''
        获取母版页中的定位符的xml中获取的字体信息
        (拉丁字体名称，中文字体名称，字体大小，字体颜色, 字体是否加粗, 字体是否斜体, 字体是否下划线, 字体水平对齐方式, 字体的上下对齐方式)
        :Args:
            - ph_xml: 定位符的xml字符串
            例如：
            <p:sp>
                ...
                <p:txBody>
                    <a:bodyPr anchor="t"/>
                    <a:lstStyle>
                        <a:lvl1pPr marL="0" indent="0" algn="ctr">
                        <a:buNone/>
                        <a:defRPr sz="3024" b="1" i="1" u="sng">
                        <a:solidFill>
                            <a:srgbClr val="FF0000"/>
                        </a:solidFill>
                        <a:latin typeface="义启俏黑体" panose="02000800000000000000" pitchFamily="2" charset="-122"/>
                        <a:ea typeface="义启俏黑体" panose="02000800000000000000" pitchFamily="2" charset="-122"/>
                        </a:defRPr>
                    </a:lvl1pPr>
                    </a:lstStyle>
                    <a:p>
                        <a:r>
                            <a:rPr lang="en-US" dirty="0" smtClean="0"/>
                            <a:t>新的菜单文本2</a:t>
                        </a:r>
                    </a:p>
            </p:sp>

            <a:defRPr sz="3024"> 字体大小
            <a:srgbClr val="FF0000"/> 字体颜色
            <a:latin typeface="义启俏黑体"/> 拉丁字体名称
            <a:ea typeface="义启俏黑体"/> 中文字体名称

        :Return:
            字体大小=sz/100 取整
            返回字体信息，[拉丁字体名称，中文字体名称，字体大小，字体颜色, 字体是否加粗, 字体是否斜体, 字体是否下划线, 字体水平对齐方式, 字体的上下对齐方式]
    '''
    # 定义命名空间
    namespaces = {
        'a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
        'p': 'http://schemas.openxmlformats.org/presentationml/2006/main'
    }
    # 解析xml字符串
    root = etree.fromstring(ph_xml)
    # 获取字体大小
    font_size = root.xpath('.//a:defRPr/@sz', namespaces=namespaces)
    if font_size:
        font_size = int(font_size[0]) // 100
    else:
        font_size = 18
    # 获取字体颜色
    font_color = root.xpath('.//a:srgbClr/@val', namespaces=namespaces)
    if font_color:
        font_color = font_color[0]
    else:
        font_color = '000000'
    # 获取拉丁字体名称
    latin_font = root.xpath('.//a:latin/@typeface', namespaces=namespaces)
    if latin_font:
        latin_font = latin_font[0]
    else:
        latin_font = '微软雅黑'
    # 获取中文字体名称
    ea_font = root.xpath('.//a:ea/@typeface', namespaces=namespaces)
    if ea_font:
        ea_font = ea_font[0]
    else:
        ea_font = '微软雅黑'

    # 获取字体是否加粗
    font_bold = root.xpath('.//a:defRPr/@b', namespaces=namespaces)
    if font_bold:
        font_bold = int(font_bold[0])
    else:
        font_bold = 0
    
    # 获取字体是否斜体
    font_italic = root.xpath('.//a:defRPr/@i', namespaces=namespaces)
    if font_italic:
        font_italic = int(font_italic[0])
    else:
        font_italic = 0

    # 获取字体是否下划线
    font_underline = root.xpath('.//a:defRPr/@u', namespaces=namespaces)
    if font_underline:
        font_underline = font_underline[0] # 'sng' 单下划线 'dbl' 双下划线
    else:
        font_underline = 0

    # 获取字体水平对齐方式
    font_align = root.xpath('.//a:lvl1pPr/@algn', namespaces=namespaces)
    if font_align:
        font_align = font_align[0] # 'l' 左对齐, 'r' 右对齐, 'ctr' 水平居中
    else:
        font_align = 0

    # 获取字体的上下对齐方式
    font_v_align = root.xpath('.//a:bodyPr/@anchor', namespaces=namespaces)
    if font_v_align:
        font_v_align = font_v_align[0] # 'ctr' 中部对齐, 't' 顶端对齐, 'b' 底端对齐
    else:
        font_v_align = 0
    
    return [latin_font, ea_font, font_size, font_color, font_bold, font_italic, font_underline, font_align, font_v_align]

# 获取母版页中的定位符的字体信息
def get_master_placehold_font_info(slide_layout, placeholder_idx:int=0) -> list:
    '''
        获取母版页中某个版式的定位符的字体信息
        :Args:
            - slide_layout: 幻灯片母版中的版式
            - placeholder_idx: 定位符的索引
        :Return:
    '''
    _font_lst = []

    # 获取文本占位符
    placeholder = None
    for shape in slide_layout.shapes:
        if shape.is_placeholder and shape.placeholder_format.idx == placeholder_idx:
            # 获取文本
            print(f"get_master_placehold_font_info -》 占位符 text: {shape.text}")
            placeholder = shape
            break

    if placeholder is not None:
        # 获取占位符中的xmlstring
        _phf = placeholder.placeholder_format
        ph_xml = get_placeholder_xml(slide_layout, _phf.idx)
        # print(f"get_master_placehold_font_info -> 占位符_xml: {ph_xml}")

        _font_lst = get_master_placehold_font_info_by_xml(ph_xml)
        print(f"get_master_placehold_font_info -> 占位符 font style: {_font_lst}")

    else:
        print("未找到指定的文本占位符")

    return _font_lst

# 获取母版页中的定位符的相关信息
def get_master_placehold_info(slide_layout, placeholder_idx:int=0) -> list:
    '''
        获取母版页中某个版式的定位符的相关信息, [[left, top, width, height], [拉丁字体名称，中文字体名称，字体大小，字体颜色, 字体是否加粗, 字体是否斜体, 字体是否下划线, 字体水平对齐方式, 字体的上下对齐方式]]
        :Args:
            - slide_layout: 幻灯片母版中的版式
            - placeholder_idx: 定位符的索引
        :Return:
    '''
    print(ut.cco(f'== 幻灯片母版-版式名称:', 'green'), slide_layout.name)
    _ph_info = []
    # 获取文本占位符的位置和长宽信息
    _ph_box = get_master_placehold_box(slide_layout, placeholder_idx)
    # 获取文本占位符的字体信息
    _ph_font = get_master_placehold_font_info(slide_layout, placeholder_idx)
    _ph_info.append(_ph_box)
    _ph_info.append(_ph_font)

    return _ph_info


# 测试母版页中的定位符
def testcpp():
    '''
        测试
        1- 先使用createpptx，创建一个pptx文件
        2- 使用editMasterLayout，修改母版页中的定位符
        :Args:
            - var: 描述
        :Return:
    '''
    pptxfile = os.path.join(_pptxtemppath, 'tp1.pptx') # 模板文件
    
    prs = Presentation(pptxfile)
    # sld = add_cover(prs)
    # 获取第一个slide的所有文本框的字体信息
    get_textbox_font_info(prs.slides[0])

    slide_master = prs.slide_master
    slide_layout = slide_master.slide_layouts[1] # 母版页中的第二个版式

    menu_info = get_master_placehold_info(slide_layout, 10)
    print(f"母版页中的定位符的相关信息: {menu_info}")
    # 使用slide_layout版式增加一个slide
    _newslide = prs.slides.add_slide(slide_layout)

    # 获取新slide的所有占位符的信息
    print(ut.cco(f'== 新slide的所有占位符的信息:', 'green'))
    for ph in _newslide.placeholders:
        phf = ph.placeholder_format
        print(f"占位符：{phf.idx}---{phf.type}；文本：{ph.text}")

    # 设置新slide的标题idx=0和内容idx=11
    _newslide.placeholders[0].text = "2025年度计划"
    _newslide.placeholders[10].text = "菜单1-新能源发展"
    _newslide.placeholders[11].text = "2025年度计划内容,发展新能源，推动绿色发展，发展人工智能。降低碳排放，提高能源利用率。"

    # 插入一个font awesome图标到idx=13的占位符中
    paragraph13 = _newslide.placeholders[13].text_frame.paragraphs[0]
    paragraph13.text = '\u2615'
    # 设置字体名称为Font Awesome 6 Free Regular
    # paragraph13.font.name = 'Segoe UI Emoji'
    # 设置字体大小
    # paragraph13.font.size = Pt(48)

    # 插入一个图片到idx=18的占位符中
    imgpath = os.path.join(os.path.dirname(__file__), 'static/images/dblogomask.png')
    _newslide.placeholders[12].insert_picture(imgpath)

    _newpptxfile = os.path.join(_pptoutpath, ut.CURDAY+"_test.pptx")
    prs.save(_newpptxfile)

    print(ut.cco(f'== pptx文件:', 'green'), _newpptxfile)

    # 调用系统的PPT软件打开文件
    os.system(f'start {_newpptxfile}')

# func
    

def add_slide(prs:presentation.Presentation, masterlayout:int=0) -> slide.Slide:
    '''
        使用母版中某个版式添加一个幻灯片
        :Args:
            - prs: Presentation对象
            - masterlayout: 幻灯片母版编号
        :Return:
            - slide.Slide对象
    '''
    slide_master = prs.slide_master
    slide_layout = slide_master.slide_layouts[masterlayout]

    print(ut.cco(f'== 幻灯片母版-版式名称及数量', 'green'), slide_layout.name, len(slide_master.slide_layouts))

    slide = prs.slides.add_slide(slide_layout)
    
    return slide

# 增加slide中的主题
def add_cover(slide:slide.Slide, title:str=None, content:str=None) -> None:
    '''
        添加幻灯片的标题和内容
        :Args:
            - slide: slide.Slide对象
            - title: 标题
            - content: 内容
        :Return:
            - None
    '''
    idx = slide.slide_id
    print(ut.cco(f'== 第{idx}页的占位符ID:', 'green'))
    for ph in slide.placeholders:
        phf = ph.placeholder_format
        print(f"定位符：{phf.idx}---{phf.type}")
    
    title_shape = slide.shapes.title
    # 主标题
    title_shape.text = title
    # 副标题
    subtitle_shape = slide.placeholders[1]
    subtitle_shape.text = content

    # 使用menu在封面中增加一个菜单，将menu中的内容填充到封面页面的第三个占位符中
    '''
        封面页面的占位符：菜单占位符idx=10
        根据menu菜单列表，循环复制占位符idx=10，每个菜单项占位符与第一个占位符左对齐，上下间距为1cm
    '''
    menu_ph_len = len(menu)
    for i in range(menu_ph_len):
        if i == 0:
            ph = slide.placeholders[10]
        else:
            ph = slide.shapes.add_textbox(Inches(1), Inches(1 + i * 1.5), Inches(5), Inches(1))
        ph.text = menu[i]


def maintest():
    '''
        主测试入口
        :Return: None
    '''
    # 创建一个pptx文件
    pptxfile = createpptx()
    print(f'生成PPTX文件: {pptxfile}')
    # 打开pptx文件，添加封面， 设置标题和副标题
    prs = Presentation(pptxfile)
    slide = add_slide(prs, 0)
    add_cover(slide, '世界这么大', '副标题：我想去看看')
    prs.save(pptxfile)

# maintest



if __name__ == '__main__':
    # maintest()
    # read_md()
    testcpp()
